mod db;

use chrono::Local;
use mysql::from_row;
use mysql::prelude::Queryable;
use std::fs;
use std::fs::File;
use std::io::Write;
use std::ptr::replace;
use crate::db::{get_conn, get_dbname_tables};

const COVER_EXIST_FILES: bool = true;

struct TableColumnInfo {
    pub column_name: String,
    pub data_type: String,
    pub character_maximum_length: Option<i64>,
    pub is_nullable: String,
    pub column_default: Option<i64>,
    pub extra: Option<String>,
    pub pk: bool,
}

fn gen_model_struct_field(table_column_info: &TableColumnInfo) -> (String, bool) {
    let mut line = "\tpub ".to_owned() + &*table_column_info.column_name + ":";
    let mut have_time_field = false;

    if table_column_info.data_type.contains("char")
        || table_column_info.data_type.contains("text")
        || table_column_info.data_type.contains("blob")
        || table_column_info.data_type.contains("binary")
        || table_column_info.data_type.contains("enum")
        || table_column_info.data_type.contains("set")
    {
        line += "\tOption<String>";
    } else if table_column_info.data_type.contains("int") && table_column_info.data_type != "bigint"
    {
        line += "\tOption<i32>"
    } else if table_column_info.data_type == "bigint" {
        line += "\tOption<i64>"
    } else if table_column_info.data_type.contains("date")
        || table_column_info.data_type.contains("time")
        || table_column_info.data_type.contains("datetime")
        || table_column_info.data_type.contains("timestamp")
        || table_column_info.data_type.contains("year")
    {
        line += "\tOption<NaiveDateTime>";
        have_time_field = true
    }
    line += ",";
    (line, have_time_field)
}

fn model_concat_cols_make_line(table_column_info: &TableColumnInfo) -> String {
    let line1 =
        "\t\tif param.".to_owned() + &*table_column_info.column_name.clone() + ".is_some() {\n";
    let line2 = "\t\t\ts += &*(conj.to_owned() + \"".to_owned()
        + &*table_column_info.column_name.clone()
        + " = ?\");\n";
    let line3 = "\t\t\tv.push(param.".to_owned()
        + &*table_column_info.column_name
        + ".clone().unwrap().to_string());\n";
    let line4 = "\t\t}\n";

    return line1 + &*line2 + &*line3 + line4;
}

fn model_insert_line(table_column_info: &TableColumnInfo) -> String {
    "\t\t\t\tparam.".to_owned()
        + &*table_column_info.column_name
        + ".clone().unwrap().to_string(),\n"
}

fn model_query_line(table_column_info: &TableColumnInfo) -> String {
    "\t\t\t\t\t\t".to_owned()
        + &*table_column_info.column_name
        + ": Some("
        + &*table_column_info.column_name
        + "),\n"
}

fn gen_model_file(table_name: String, table_column_infos: &Vec<TableColumnInfo>) {
    let mut fields = String::from("");
    let mut have_time_field = false;
    let mut insert_lines = String::from("");
    let mut filed_names = String::from("");
    let mut prep_names = String::from("");
    let mut query_lines = String::from("");
    let mut model_concat_cols_make_lines = String::from("");

    for table_column_info in table_column_infos {
        let (field, have_time_field_sub) = gen_model_struct_field(table_column_info);
        if have_time_field_sub {
            have_time_field = true;
        }
        fields += &*("\n".to_owned() + &*field);
        insert_lines += &*model_insert_line(table_column_info);
        filed_names += &*(table_column_info.column_name.clone() + ",");
        prep_names += "?,";
        query_lines += &*model_query_line(table_column_info);
        model_concat_cols_make_lines += &*model_concat_cols_make_line(table_column_info);
    }
    filed_names = filed_names[..filed_names.len() - 1].to_owned();
    prep_names = prep_names[..prep_names.len() - 1].to_owned();
    let table_name_lower = table_name.to_lowercase();
    let table_name_capital = capitalize_string(&table_name);

    let mut use_library = "";
    if have_time_field {
        use_library = "use chrono::NaiveDateTime;\n";
    }

    let file_content = MODEL_TEMPLATE
        .replace("{%table_name_capital%}", &*table_name_capital)
        .replace("{%table_name_lower%}", &*table_name_lower)
        .replace("{%fields%}", &*fields)
        .replace("{%use_library%}", use_library)
        .replace("{%insert_lines%}", &*insert_lines)
        .replace("{%filed_names%}", &*filed_names)
        .replace("{%prep_names%}", &*prep_names)
        .replace("{%query_lines%}", &*query_lines)
        .replace(
            "{%model_concat_cols_make_lines%}",
            &*model_concat_cols_make_lines,
        );

    let filename = table_name_lower + ".rs";
    let dir = "src/models/";
    gen_file(dir, filename, &*file_content)
}

fn gen_service_file(table_name: String, table_column_infos: &Vec<TableColumnInfo>) {
    let table_name_lower = table_name.to_lowercase();
    let table_name_capital = capitalize_string(&table_name);

    let file_content = SERVICE_TEMPLATE
        .replace("{%table_name_capital%}", &*table_name_capital)
        .replace("{%table_name_lower%}", &*table_name_lower);
    let filename = table_name_lower + ".rs";
    let dir = "src/services/";
    gen_file(dir, filename, &*file_content);
}

fn gen_controller_file(table_name: &String) {
    let table_name_lower = table_name.to_lowercase();
    let table_name_capital = capitalize_string(table_name);

    let file_content = CONTROLLER_TEMPLATE
        .replace("{%table_name_capital%}", &*table_name_capital)
        .replace("{%table_name_lower%}", &*table_name_lower);

    let filename = table_name_lower + ".rs";
    let dir = "src/controllers/";
    gen_file(dir, filename, &file_content)
}

fn gen_table_fields(dbname: String, tables: Vec<String>) {
    let mut conn = get_conn();
    for table in tables {
        let pri_sql_tpl = r#"SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
                                    WHERE TABLE_SCHEMA=? AND TABLE_NAME=? AND COLUMN_KEY='PRI';"#;
        let pri_sql_stmt = conn.prep(pri_sql_tpl);
        let pk_name = conn
            .exec_first(pri_sql_stmt.unwrap(), (dbname.clone(), table.clone()))
            .map(|row| {
                let pk_name: String = row.unwrap();
                pk_name
            })
            .unwrap();

        let sql = "SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, IS_NULLABLE, COLUMN_DEFAULT,EXTRA
                                FROM information_schema.COLUMNS
                                WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?";
        let stmt = conn.prep(sql);
        let mut table_column_infos = vec![];
        conn.exec_iter(stmt.unwrap(), (dbname.clone(), table.clone()))
            .unwrap()
            .for_each(|row| {
                let r: (
                    String,
                    String,
                    Option<i64>,
                    String,
                    Option<i64>,
                    Option<String>,
                ) = from_row(row.unwrap());
                let t = TableColumnInfo {
                    column_name: r.0.clone().to_lowercase(),
                    data_type: r.1,
                    character_maximum_length: r.2,
                    is_nullable: r.3,
                    column_default: r.4,
                    extra: r.5,
                    pk: r.0 == pk_name,
                };
                table_column_infos.push(t);
            });

        gen_model_file(table.clone(), &table_column_infos);
        gen_service_file(table.clone(), &table_column_infos);
        gen_controller_file(&table);
        // gen_router_file(table.clone());
    }
}

fn check_files_exist(tables: &Vec<String>) -> bool {
    let dirs = vec![
        "src/controllers/",
        "src/models/",
        "src/services/",
        "src/routers/",
    ];
    let mut exist_files = vec![];
    for dir in dirs {
        for table in tables {
            let pathname = dir.to_owned() + &*table.to_lowercase() + ".rs";
            if fs::metadata(&pathname).is_ok() {
                exist_files.push(pathname);
            }
        }
    }

    if exist_files.len() > 0 {
        println!("待生成MVC文件已有存在, 请先移除: ");
        for file in exist_files {
            println!("{}", "\t".to_owned() + &*file);
        }
        return true;
    }

    return false;
}

fn connect_db_and_show_tables() -> (String, Vec<String>) {
    get_dbname_tables()
}

pub fn main() {
    let (dbname, tables) = connect_db_and_show_tables();
    if tables.len() < 1 {
        println!("Done.Empty table list.");
        return;
    }

    if !COVER_EXIST_FILES && check_files_exist(&tables) {
        return;
    }

    gen_table_fields(dbname, tables);
    println!("Done.Success.")
}

fn gen_file(dir: &str, filename: String, content: &str) {
    fs::create_dir_all(dir).expect("TODO: panic message");
    let mut file = File::create(dir.to_owned() + &*filename).expect("无法创建文件");
    file.write_all(content.as_bytes())
        .expect("TODO: panic message");
}

fn capitalize_string(s: &String) -> String {
    let lw = s.to_lowercase();
    if s.len() == 0 {
        return s.clone();
    }

    lw[..1].to_uppercase() + &lw[1..]
}

const MODEL_TEMPLATE: &str = r#"
use crate::models::db_connect::get_conn;
use mysql::prelude::Queryable;
use mysql::{from_row, params};
use rocket::serde::json::Json;
use rocket::Error;
use serde::{Deserialize, Serialize};
use std::string::String;
{%use_library%}

#[derive(Serialize, Deserialize)]
pub struct {%table_name_capital%}Model {
    {%fields%}
}

impl {%table_name_capital%}Model {
    pub fn add(param: Json<{%table_name_capital%}Model>) -> Result<bool, Error> {
        let mut conn = get_conn();
        conn.exec_iter(
            "INSERT INTO {%table_name_lower%}({%filed_names%}) VALUES ({%prep_names%})",
            (
{%insert_lines%}
            ),
        )
        .expect("TODO: panic message");

        Ok(true)
    }

    pub fn query(param: Json<{%table_name_capital%}Model>) -> Result<Vec<{%table_name_capital%}Model>, Error> {
        let mut conn = get_conn();
        let conj = "and";
        let (mut sql_ext, param_values) = {%table_name_capital%}Model::concat_cols_make(param, conj);
        if sql_ext.starts_with(conj) {
            sql_ext = sql_ext[conj.len()..].parse().unwrap()
        }
        let pre_sql: String = String::from("SELECT * FROM {%table_name_lower%} where ") + &*sql_ext;
        let stmt = conn.prep(pre_sql);

        let mut {%table_name_lower%}s = vec![];
        conn.exec_iter(stmt.unwrap(), param_values)
            .unwrap()
            .for_each(|row| {
                let ({%filed_names%}) = from_row(row.unwrap());
                let query_item = {%table_name_capital%}Model {
{%query_lines%}                    };
                {%table_name_lower%}s.push(query_item);
            });

        Ok({%table_name_lower%}s)
    }

    pub fn query_by_pk(id: i32) -> Result<Option<{%table_name_capital%}Model>, Error> {
        let mut conn = get_conn();
        let {%table_name_lower%} = conn
            .exec_first(
                "SELECT * FROM {%table_name_lower%} WHERE id = :id",
                params! {
                    "id" => id
                },
            )
            .map(|row| {
                row.map(|({%filed_names%})| {%table_name_capital%}Model {
{%query_lines%}                })
            });

        Ok({%table_name_lower%}.unwrap())
    }

    pub fn update(param: Json<{%table_name_capital%}Model>) -> Result<bool, Error> {
        let mut conn = get_conn();
        let conj = ",";
        let pk = param.id.clone().unwrap().to_string();
        let (mut sql_ext, mut param_values) = {%table_name_capital%}Model::concat_cols_make(param, conj);
        if sql_ext.starts_with(conj) {
            sql_ext = sql_ext[conj.len()..].parse().unwrap()
        }
        param_values.push(pk);
        let pre_sql: String = String::from("UPDATE {%table_name_lower%} SET ") + &*sql_ext + " WHERE id = ? ";
        let stmt = conn.prep(pre_sql);

        conn.exec_iter(stmt.unwrap(), param_values)
            .expect("TODO: panic message");

        Ok(true)
    }

    pub fn delete(id: i32) -> Result<bool, Error> {
        let mut conn = get_conn();
        conn.exec_iter(
            "DELETE FROM {%table_name_lower%} WHERE id = :id",
            params! {
                "id" => id
            },
        )
        .unwrap();

        Ok(true)
    }

    fn concat_cols_make(param: Json<{%table_name_capital%}Model>, conj: &str) -> (String, Vec<String>) {
        let mut s = "".to_string();
        let mut v = vec![];
{%model_concat_cols_make_lines%}
        (s, v)
    }
}


"#;

const SERVICE_TEMPLATE: &str = r#"
use crate::models::{%table_name_lower%}::{%table_name_capital%}Model;
use mysql::prelude::Queryable;
use rocket::serde::json::Json;
use rocket::Error;

pub struct {%table_name_capital%}Service;

impl {%table_name_capital%}Service {
    pub fn add(param: Json<{%table_name_capital%}Model>) -> Result<bool, Error> {
        {%table_name_capital%}Model::add(param)
    }

    pub fn query(param: Json<{%table_name_capital%}Model>) -> Result<Vec<{%table_name_capital%}Model>, Error> {
        let {%table_name_lower%}s = {%table_name_capital%}Model::query(param);
        {%table_name_lower%}s
    }

    pub fn query_by_pk(id: i32) -> Result<Option<{%table_name_capital%}Model>, Error> {
        let {%table_name_lower%}s = {%table_name_capital%}Model::query_by_pk(id);
        {%table_name_lower%}s
    }

    pub fn update(param: Json<{%table_name_capital%}Model>) -> Result<bool, Error> {
        {%table_name_capital%}Model::update(param)
    }

    pub fn delete(id:i32) -> Result<bool, Error> {
        {%table_name_capital%}Model::delete(id)
    }
}

"#;

const CONTROLLER_TEMPLATE: &str = r#"
use crate::services::{%table_name_lower%};
use rocket::{response, Build, Request, Response};
use rocket::form::Form;
use rocket::http::ContentType;
use rocket::response::{content, Responder};
use crate::models::{%table_name_lower%}::{%table_name_capital%}Model;
use crate::services::{%table_name_lower%}::{%table_name_capital%}Service;
use rocket::serde::json::{Json, Value, json};

#[post("/add", format = "json", data = "<param>")]
fn add<'r>(param: Json<{%table_name_capital%}Model>) -> content::RawJson<String> {
    let res = {%table_name_capital%}Service::add(param).unwrap();
    let s = serde_json::to_string(&res).unwrap();
    content::RawJson(s.parse().unwrap())
}

#[post("/query", format = "json", data = "<param>")]
fn query<'r>(param: Json<{%table_name_capital%}Model>) -> content::RawJson<String> {
    let {%table_name_lower%}s = {%table_name_capital%}Service::query(param).unwrap();
    let s = serde_json::to_string(&{%table_name_lower%}s).unwrap();
    content::RawJson(s)
}

#[post("/query_by_pk", format = "json", data = "<param>")]
fn query_by_pk<'r>(param: Json<{%table_name_capital%}Model>) -> content::RawJson<String> {
    let {%table_name_lower%}s = {%table_name_capital%}Service::query_by_pk(param.id.unwrap()).unwrap();
    let s = serde_json::to_string(&{%table_name_lower%}s).unwrap();
    content::RawJson(s)
}

#[post("/update", format = "json", data = "<param>")]
fn update<'r>(param: Json<{%table_name_capital%}Model>) -> content::RawJson<String> {
    let {%table_name_lower%}s = {%table_name_capital%}Service::update(param).unwrap();
    let s = serde_json::to_string(&{%table_name_lower%}s).unwrap();
    content::RawJson(s.parse().unwrap())
}

#[post("/delete", format = "json", data = "<param>")]
fn delete<'r>(param: Json<{%table_name_capital%}Model>) -> content::RawJson<String> {
    let {%table_name_lower%}s = {%table_name_capital%}Service::delete(param.id.unwrap()).unwrap();
    let s = serde_json::to_string(&{%table_name_lower%}s).unwrap();
    content::RawJson(s.parse().unwrap())
}

pub fn mount(rocket: rocket::Rocket<Build>) -> rocket::Rocket<Build> {
    let rocket = rocket.mount("/{%table_name_lower%}", routes![add, query, query_by_pk, update, delete]);
    rocket
}

"#;
